package com.xiang.ad.index.keyword;

import com.xiang.ad.index.IndexAware;
import com.xiang.ad.utils.CommonUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;

/**
 * Created by xiang.
 * 推广单元-关键词 的两个index 的crud
 * 推广单元的限制维度  使用倒排索引，以关键词的形式寻找推广单元的id，关键词&推广单元的id之间的关系是多对多的
 */
@Slf4j
@Component
public class UnitKeywordIndex implements IndexAware<String, Set<Long>> {//value是保存unitId的set

    // 两个map 存 两个index
    private static Map<String, Set<Long>> keywordUnitMap;//keyword-unit的索引  倒排索引
    private static Map<Long, Set<String>> unitKeywordMap;//unit-keyword的索引  正向索引


    // 因为监听 Binlog 更新数据需要线程安全，所以需要线程安全的容器。
    // 使用ConcurrentHashMap对两个map初始化
    static {
        keywordUnitMap = new ConcurrentHashMap<>();//keyword-unit的索引
        unitKeywordMap = new ConcurrentHashMap<>();//unit-keyword的索引
    }

    // 通过关键词 获取 推广单元
    @Override
    public Set<Long> get(String key) {

        if (StringUtils.isEmpty(key)) {//传入的key为空，没有命中任何 推广单元
            return Collections.emptySet();
        }

        Set<Long> result = keywordUnitMap.get(key);
        if (result == null) {// 这个关键词不存在任何推广单元与之关联
            return Collections.emptySet();//返回空的集合
        }
        //
        return result;
    }

    // 添加index  如果直接从 MySQL 获取数据构建索引，那么当多个实例的情况下，会给 MySQL 造成巨大的压力，造成性能降低。
    @Override
    public void add(String key, Set<Long> value) { // key是keyword，value是unitId

        log.info("UnitKeywordIndex, before add: {}", unitKeywordMap);

        //添加 关键词 到 推广单元的 索引
        Set<Long> unitIdSet = CommonUtils.getorCreate(//利用工具类
                key,    //key
                keywordUnitMap, //map
                //由于同时需要去更新两个map，更新的时候map通过k去获取v的时候，可能会不存在；
                //当不存在的时候,创建一个set，这个set也需要是线程安全的，所以使用ConcurrentSkipListSet
                ConcurrentSkipListSet::new
        );
        unitIdSet.addAll(value);//填入 推广单元

        // 添加 推广单元 到 关键词的索引
        for (Long unitId : value) {//对推广单元的id进行遍历
            Set<String> keywordSet = CommonUtils.getorCreate(
                    unitId,   //key
                    unitKeywordMap,  //map
                    ConcurrentSkipListSet::new  //set存的就是value
            );
            keywordSet.add(key);//填入 关键词
        }

        //打印日志，用于对比
        log.info("UnitKeywordIndex, after add: {}", unitKeywordMap);
    }

    // 更新，不允许更新
    // 因为 关键词的更新，会导致两个map发生改变，而且每一个map都会牵扯到一个set，需要对set遍历，所以更新成本非常高
    @Override
    public void update(String key, Set<Long> value) {

        log.error("keyword index 不支持更新，请删除或者重新添加");
    }

    // 删除索引
    @Override
    public void delete(String key, Set<Long> value) {//这里传入的key可能是一部分UnitIds，所以不能直接通过map去remove

        log.info("UnitKeywordIndex, before delete: {}", unitKeywordMap);

        //部分删除 keyword对应的unitId
        Set<Long> unitIds = CommonUtils.getorCreate(//首先尝试获取，如果存在会get到这个set，不存在就new一个新的set
                key,
                keywordUnitMap,
                ConcurrentSkipListSet::new
        );
        unitIds.removeAll(value);// 因为一个 keyword 可以有多个 unitId 对应。这里只是删除 unitId，不能一次性删掉所有的 keyword；

        //部分删除 unitId对应的keyword
        for (Long unitId : value) {

            Set<String> keywordSet = CommonUtils.getorCreate(//首先尝试获取，如果存在会get到这个set，不存在就new一个新的set
                    unitId,
                    unitKeywordMap,
                    ConcurrentSkipListSet::new
            );
            keywordSet.remove(key); //
        }

        log.info("UnitKeywordIndex, after delete: {}", unitKeywordMap);
    }

    // 匹配方法  匹配某一个推广单元是否关联了所有关键词
    // 当这个推广单元的id，对应的推广单元包含了所有的关键词的限制 才会返回true
    public boolean match(Long unitId, List<String> keywords) {
        //是否包含了推广单元的id
        if (unitKeywordMap.containsKey(unitId)
                && CollectionUtils.isNotEmpty(unitKeywordMap.get(unitId))) {
            //尝试获取这个推广单元所对应的关键词的限制
            Set<String> unitKeywords = unitKeywordMap.get(unitId);

            //apache 提供的工具类 
            //判断子集合是否是一个另一个的子集   这里当且仅当keywords是unitKeywords子集的时候，返回true
            return CollectionUtils.isSubCollection(keywords, unitKeywords);
        }
        //
        return false;
    }
}
